using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Magiq.Tests.Support;
using Magiq.Tests.TestModel;
using Magiq.Update;
using NUnit.Framework;

namespace Magiq.Tests {
    public abstract class BaseMagiqProviderUpdatesTest<T, TI> : BaseMagiqProviderTest<T, TI>
        where T : class, ITestClass1<T>, new() where TI : ITestClass1Inheritor<T>, T, new() {
        [Test]
        public void ShouldUpdateStringProperties() {
            TestUpdate(TestClass1s,
                       s => s.Set(x => x.StringProperty, "q"),
                       x => x.StringProperty == "q");
        }

        [Test]
        public void ShouldUpdateIntProperties() {
            TestUpdate(TestClass1s,
                       s => s.Set(x => x.IntProperty, 3),
                       x => x.IntProperty == 3);
        }

        [Test]
        public void ShouldUpdateReferencesAsValues() {
            var r = TestClass2s.First();
            TestUpdate(TestClass1s,
                       s => s.Set(x => x.Reference, r),
                       x => x.Reference == r);
        }

        [Test]
        public void ShouldUpdateChainedReferences() {
            TestUpdate(TestClass1s,
                       x => x.StringProperty == "sarasa",
                       s => s.Set(x => x.Reference.StringProperty, "lala"),
                       items => items.Where(x => x.Reference != null).Select(x => x.Reference).Distinct().Count(),
                       x => x.Reference != null && x.Reference.StringProperty == "lala");
        }

        [Test]
        public void ShouldUpdateSelfReferences() {
            TestUpdate(TestClass1s,
                       x => x.StringProperty == "sarasa",
                       s => s.Set(x => x.SelfReference.StringProperty, "self changed"),
                       items =>
                       items.Where(x => x.SelfReference != null).Select(x => x.SelfReference).Distinct().Count(),
                       x => x.SelfReference != null && x.SelfReference.StringProperty == "self changed");
        }

        [Test]
        public void ShouldUpdateWhereThatUsesSelfReference() {
            TestUpdate(TestClass1s,
                       x => x.SelfReference != null && x.SelfReference.IntProperty > 3,
                       x => x.Set(u => u.StringProperty, "lala"),
                       x => x.StringProperty == "lala");
        }

        [Test]
        public void ShouldUpdateChainedReferencesWithOneSingleSetThatInvalidatesTheWhereClause() {
            TestUpdate(TestClass1s,
                       x => x.StringProperty == "sarasa",
                       s => s.Set(x => x.StringProperty, "lolo")
                                .Set(x => x.Reference.StringProperty, "lala"),
                       items =>
                       items.Where(x => x.Reference != null).Select(x => x.Reference).Distinct().Count() + items.Count());
        }

        [Test]
        public void ShouldUpdateChainedReferencesWithSeveralSetsThatInvalidatesTheWhereClause() {
            TestUpdate(TestClass1s,
                       x => x.StringProperty == "sarasa" && x.IntProperty == 3,
                       s => s.Set(x => x.StringProperty, "lolo")
                                .Set(x => x.IntProperty, 5)
                                .Set(x => x.Reference.StringProperty, "lala"),
                       items =>
                       items.Where(x => x.Reference != null).Select(x => x.Reference).Distinct().Count() + items.Count());
        }

        [Test]
        public void ShouldUpdateReferencesAsExpressions() {
            var r = TestClass2s.First();
            TestUpdate(TestClass1s,
                       s => s.Set(x => x.Reference, x => r),
                       x => x.Reference == r);
        }


        [Test]
        public void ShouldUpdateReferenceEqualToSourceReference() {
            TestUpdate(TestClass1s,
                       s => s.Set(x => x.Reference, x => x.Reference),
                       x => true);
        }


        [Test]
        public void ShouldUpdateReferenceEqualToAnotherSourceReference() {
            TestUpdate(TestClass1s,
                       s => s.Set(x => x.Reference2, x => x.Reference),
                       x => true);
        }

        [Test]
        public void ShouldUpdateSeveralProperties() {
            var r = TestClass2s.First();
            TestUpdate(TestClass1s,
                       s => s.Set(x => x.IntProperty, 3)
                                .Set(x => x.StringProperty, "lalala")
                                .Set(x => x.Reference, r),
                       x => x.Reference == r && x.IntProperty == 3 && x.StringProperty == "lalala");
        }

        [Test]
        public void ShouldUpdateSeveralEntities() {
            var items = TestClass1s.Where(x => x.StringProperty == "sarasa");
            var count = items.Where(x => x.Reference != null).Select(x => x.Reference).Distinct().Count() +
                        items.Count();
            items.Set(x => x.IntProperty, 3)
                .Set(x => x.AnotherStringProperty, "pe")
                .Set(x => x.Reference.StringProperty, "lala")
                .Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesStringProperty() {
            TestUpdate(TestClass1s,
                       x => x.StringProperty == "sarasa",
                       x => x.Set(u => u.StringProperty, "lala"),
                       x => x.StringProperty == "lala");
        }

        [Test]
        public void ShouldUpdateComplexWhere() {
            TestUpdate(TestClass1s,
                       x =>
                       (x.StringProperty == "sarasa" && x.IntProperty >= 3) ||
                       x.AnotherIntProperty < 12 && x.AnotherStringProperty != null,
                       x => x.Set(u => u.StringProperty, "lala"),
                       x => x.StringProperty == "lala");
        }

        [Test]
        public void ShouldUpdateWhereThatUsesAnotherStringProperty() {
            TestUpdate(TestClass1s,
                       x => x.StringProperty == x.AnotherStringProperty,
                       x => x.Set(u => u.StringProperty, "lala"),
                       x => x.StringProperty == "lala");
        }


        [Test]
        public void ShouldUpdateWhereThatUsesStringPropertyNull() {
            TestUpdate(TestClass1s,
                       x => x.StringProperty == null,
                       x => x.Set(u => u.StringProperty, "lala"),
                       x => x.StringProperty == "lala");
        }

        [Test]
        public void ShouldUpdateWhereThatUsesComposedStringProperty() {
            TestUpdate(TestClass1s,
                       x => x.StringProperty == x.AnotherStringProperty + "la",
                       x => x.Set(u => u.StringProperty, "lala"),
                       x => x.StringProperty == "lala");
        }

        [Test]
        public void ShouldUpdateWithComposedStringProperty() {
            TestUpdate(TestClass1s,
                       x => x.StringProperty == "sarasa",
                       s => s.Set(x => x.StringProperty, x => x.StringProperty + "lala"),
                       x => x.StringProperty == "sarasalala");
        }

        [Test]
        public void ShouldUpdateWithComposedStringProperties() {
            TestUpdate(TestClass1s,
                       x => x.StringProperty == "sarasa",
                       s => s.Set(x => x.StringProperty, x => x.StringProperty + x.AnotherStringProperty),
                       x => x.StringProperty == "sarasa" + x.AnotherStringProperty);
        }

        [Test]
        public void ShouldUpdateFromAnotherStringProperty() {
            TestUpdate(TestClass1s,
                       x => x.StringProperty == "sarasa",
                       s => s.Set(x => x.StringProperty, x => x.AnotherStringProperty),
                       x => x.StringProperty == x.AnotherStringProperty);
        }

        [Test]
        public void ShouldUpdateWithComposedIntProperty() {
            TestUpdate(TestClass1s,
                       x => x.IntProperty == 3,
                       s => s.Set(x => x.IntProperty, x => x.IntProperty + 5),
                       x => x.IntProperty == 8);
        }

        [Test]
        public void ShouldUpdateWithComposedIntProperties() {
            TestUpdate(TestClass1s,
                       x => x.IntProperty == 3,
                       s => s.Set(x => x.IntProperty, x => x.IntProperty + x.AnotherIntProperty),
                       x => x.IntProperty == 3 + x.AnotherIntProperty);
        }

        [Test]
        public void ShouldUpdateFromAnotherIntProperty() {
            TestUpdate(TestClass1s,
                       x => x.IntProperty == 3,
                       s => s.Set(x => x.IntProperty, x => x.AnotherIntProperty),
                       x => x.IntProperty == x.AnotherIntProperty);
        }

        [Test]
        public void ShouldUpdateWhereThatUsesIntProperty() {
            var items = TestClass1s.Where(x => x.IntProperty == 3);
            var count = items.Count();
            items.Set(x => x.IntProperty, 4).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesComposedIntProperty() {
            var items = TestClass1s.Where(x => x.IntProperty == x.AnotherIntProperty + 3);
            var count = items.Count();
            items.Set(x => x.IntProperty, 4).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesAnotherIntProperty() {
            var items = TestClass1s.Where(x => x.IntProperty == x.AnotherIntProperty);
            var count = items.Count();
            items.Set(x => x.IntProperty, 4).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesReference() {
            var r = TestClass2s.First();
            var items = TestClass1s.Where(x => x.Reference == r);
            var count = items.Count();
            items.Set(x => x.Reference, r).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesReferenceNull() {
            var r = TestClass2s.First();
            var items = TestClass1s.Where(x => x.Reference == null);
            var count = items.Count();
            items.Set(x => x.Reference, x => r).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateNullToReferenceAsExpressions() {
            TestUpdate(TestClass1s,
                       s => s.Set(x => x.Reference, x => null),
                       x => x.Reference == null);
        }

        [Test]
        public void ShouldUpdateNullToReferenceAsValues() {
            TestUpdate(TestClass1s,
                       s => s.Set(x => x.Reference, (TestClass2) null),
                       x => x.Reference == null);
        }

        [Test]
        public void ShouldUpdateNullToStringProperty() {
            TestUpdate(TestClass1s,
                       s => s.Set(x => x.StringProperty, x => null),
                       x => x.StringProperty == null);
        }

        [Test]
        public void ShouldUpdateNullToNullableIntProperty() {
            TestUpdate(TestClass1s,
                       s => s.Set(x => x.NullableIntProperty, x => null),
                       x => x.NullableIntProperty == null);
        }

        [Test]
        public void ShouldUpdateLongToIntProperty() {
            TestUpdate(TestClass1s,
                       s => s.Set(x => x.IntProperty, 3L),
                       x => x.IntProperty == 3);
        }

        [Test]
        public void ShouldUpdateByteToIntProperty() {
            const byte value = 3;
            TestUpdate(TestClass1s,
                       s => s.Set(x => x.IntProperty, value),
                       x => x.IntProperty == 3);
        }

        [Test]
        public void ShouldUpdateNullableIntProperties() {
            TestUpdate(TestClass1s,
                       s => s.Set(x => x.NullableIntProperty, 3),
                       x => x.NullableIntProperty == 3);
        }

        [Test]
        public void ShouldUpdateWithComposedNullableIntProperty() {
            TestUpdate(TestClass1s,
                       x => x.NullableIntProperty == 3,
                       s => s.Set(x => x.NullableIntProperty, x => x.NullableIntProperty + 5),
                       x => x.NullableIntProperty == 8);
        }

        [Test]
        public void ShouldUpdateWithComposedNullableIntProperties() {
            TestUpdate(TestClass1s,
                       x => x.NullableIntProperty == 3,
                       s => s.Set(x => x.IntProperty, x => x.NullableIntProperty + x.AnotherIntProperty),
                       x => x.IntProperty == 3 + x.AnotherIntProperty);
        }

        [Test]
        public void ShouldUpdateNullableFromAnotherIntProperty() {
            TestUpdate(TestClass1s,
                       x => x.IntProperty == 3,
                       s => s.Set(x => x.NullableIntProperty, x => x.AnotherIntProperty),
                       x => x.NullableIntProperty == x.AnotherIntProperty);
        }

        [Test]
        public void ShouldUpdateWhereThatUsesNullableIntProperty() {
            var items = TestClass1s.Where(x => x.NullableIntProperty == 3);
            var count = items.Count();
            items.Set(x => x.NullableIntProperty, 4).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesNullableIntPropertyNull() {
            var items = TestClass1s.Where(x => x.NullableIntProperty == null);
            var count = items.Count();
            items.Set(x => x.NullableIntProperty, 4).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesNullableIntPropertyWithIntProperty() {
            var items = TestClass1s.Where(x => x.NullableIntProperty == x.IntProperty);
            var count = items.Count();
            items.Set(x => x.NullableIntProperty, 4).Update().Should(Be.EqualTo(count));
        }

        [Test]
        public void ShouldUpdateWhereThatUsesNullableIntPropertyToNull() {
            var items = TestClass1s.Where(x => x.NullableIntProperty == null);
            var count = items.Count();
            items.Set(x => x.NullableIntProperty, 4).Update().Should(Be.EqualTo(count));
        }

        protected static void TestUpdate<T>(IQueryable<T> queryable, Func<IQueryable<T>, IUpdate<T>> sets,
                                            Expression<Func<T, bool>> updatedWhere) {
            TestUpdate(queryable, x => true, sets, updatedWhere);
        }

        protected static void TestUpdate<T>(IQueryable<T> queryable, Expression<Func<T, bool>> where,
                                            Func<IQueryable<T>, IUpdate<T>> sets, Expression<Func<T, bool>> updatedWhere) {
            TestUpdate(queryable, where, sets, items => items.Distinct().Count(), updatedWhere);
        }

        protected static int TestUpdate<T>(IQueryable<T> queryable, Expression<Func<T, bool>> where,
                                           Func<IQueryable<T>, IUpdate<T>> sets, Func<IEnumerable<T>, int> getCount) {
            var items = queryable.Where(where);
            var count = getCount(items);
            sets(queryable.Where(where)).Update().Should(Be.EqualTo(count));
            return count;
        }

        protected static void TestUpdate<T>(IQueryable<T> queryable, Expression<Func<T, bool>> where,
                                            Func<IQueryable<T>, IUpdate<T>> sets, Func<IEnumerable<T>, int> getCount,
                                            Expression<Func<T, bool>> updatedWhere) {
            var count = TestUpdate(queryable, where, sets, getCount);
            queryable.Where(updatedWhere).Count().Should(Be.GreaterThanOrEqualTo(count));
        }
        }
}